/*
 * @(#)RetryModule.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.IOException;

/**
 * This module handles request retries when a connection closes prematurely. It
 * is triggered by the RetryException thrown by the StreamDemultiplexor.
 * <p/>
 * This module is somewhat unique in that it doesn't strictly limit itself to
 * the HTTPClientModule interface and its return values. That is, it sends
 * request directly using the HTTPConnection.sendRequest() method. This is
 * necessary because this module will not only resend its request but it also
 * resend all other requests in the chain. Also, it rethrows the RetryException
 * in Phase1 to restart the processing of the modules.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 * @since V0.3
 */
class RetryModule implements HTTPClientModule, GlobalConstants {

    private static final Logger log = Logger.getLogger(RetryModule.class);

    // Constructors

    /**
     */
    RetryModule() {
    }

    // Methods

    /** Invoked by the HTTPClient. */
    public int requestHandler(Request req, Response[] resp) {
        return REQ_CONTINUE;
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase1Handler(Response resp, RoRequest roreq) throws IOException, ModuleException {
        try {
            resp.getStatusCode();
        } catch (RetryException re) {
            log.error("Caught RetryException");

            boolean got_lock = false;

            try {
                synchronized (re.first) {
                    got_lock = true;

                    // initialize idempotent sequence checking
                    IdempotentSequence seq = new IdempotentSequence();
                    for (RetryException e = re.first; e != null; e = e.next)
                        seq.add(e.request);

                    for (RetryException e = re.first; e != null; e = e.next) {
                        log.error("Handling exception ", e);

                        Request req = e.request;
                        HTTPConnection con = req.getConnection();

                  /*
                   * Don't retry if either the sequence is not idempotent (Sec 8.1.4
                   * and 9.1.2), or we've already retried enough times, or the headers
                   * have been read and parsed already
                   */
                        if (!seq.isIdempotent(req)
                            || (con.ServProtVersKnown && con.ServerProtocolVersion >= HTTP_1_1 && req.num_retries > 0)
                            || ((!con.ServProtVersKnown || con.ServerProtocolVersion <= HTTP_1_0) && req.num_retries > 4)
                            || e.response.got_headers) {
                            e.first = null;
                            continue;
                        }

                        /**
                         * if an output stream was used (i.e. we don't have the data
                         * to resend) then delegate the responsibility for resending
                         * to the application.
                         */
                        if (req.getStream() != null) {
                            if (HTTPConnection.deferStreamed) {
                                req.getStream().reset();
                                e.response.setRetryRequest(true);
                            }
                            e.first = null;
                            continue;
                        }

                  /*
                   * If we have an entity then setup either the entity-delay or the
                   * Expect header
                   */
                        if (req.getData() != null && e.conn_reset) {
                            if (con.ServProtVersKnown && con.ServerProtocolVersion >= HTTP_1_1)
                                addToken(req, "Expect", "100-continue");
                            else
                                req.delay_entity = 5000L << req.num_retries;
                        }

                  /*
                   * If the next request in line has an entity and we're talking to an
                   * HTTP/1.0 server then close the socket after this request. This is
                   * so that the available() call (to watch for an error response from
                   * the server) will work correctly.
                   */
                        if (e.next != null && e.next.request.getData() != null
                            && (!con.ServProtVersKnown || con.ServerProtocolVersion < HTTP_1_1) && e.conn_reset) {
                            addToken(req, "Connection", "close");
                        }

                  /*
                   * If this an HTTP/1.1 server then don't pipeline retries. The
                   * problem is that if the server for some reason decides not to use
                   * persistent connections and it does not do a correct shutdown of
                   * the connection, then the response will be ReSeT. If we did
                   * pipeline then we would keep falling into this trap indefinitely.
                   * Note that for HTTP/1.0 servers, if they don't support keep-alives
                   * then the normal code will already handle this accordingly and
                   * won't pipe over the same connection.
                   */
                        if (con.ServProtVersKnown && con.ServerProtocolVersion >= HTTP_1_1 && e.conn_reset) {
                            req.dont_pipeline = true;
                        }
                        // The above is too risky - for moment let's be safe
                        // and never pipeline retried request at all.
                        req.dont_pipeline = true;

                        // now resend the request

                        log.info("Retrying request '" + req.getMethod() + " " + req.getRequestURI() + "'");

                        if (e.conn_reset)
                            req.num_retries++;
                        e.response.http_resp.set(req, con.sendRequest(req, e.response.timeout));
                        e.exception = null;
                        e.first = null;
                    }
                }
            } catch (NullPointerException npe) {
                if (got_lock)
                    throw npe;
            } catch (ParseException pe) {
                throw new IOException(pe.getMessage());
            }

            if (re.exception != null)
                throw re.exception;

            re.restart = true;
            throw re;
        }
    }

    /** Invoked by the HTTPClient. */
    public int responsePhase2Handler(Response resp, Request req) {
        // reset any stuff we might have set previously
        req.delay_entity = 0;
        req.dont_pipeline = false;
        req.num_retries = 0;

        return RSP_CONTINUE;
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase3Handler(Response resp, RoRequest req) {
    }

    /** Invoked by the HTTPClient. */
    public void trailerHandler(Response resp, RoRequest req) {
    }

    /**
     * Add a token to the given header. If the header does not exist then create
     * it with the given token.
     *
     * @param req
     *         the request who's headers are to be modified
     * @param hdr
     *         the name of the header to add the token to (or to create)
     * @param tok
     *         the token to add
     * @throws ParseException
     *         if parsing the header fails
     */
    private void addToken(Request req, String hdr, String tok) throws ParseException {
        int idx;
        NVPair[] hdrs = req.getHeaders();
        for (idx = 0; idx < hdrs.length; idx++) {
            if (hdrs[idx].getName().equalsIgnoreCase(hdr))
                break;
        }

        if (idx == hdrs.length) // no such header, so add one
        {
            hdrs = Util.resizeArray(hdrs, idx + 1);
            hdrs[idx] = new NVPair(hdr, tok);
            req.setHeaders(hdrs);
        } else
        // header exists, so add token
        {
            if (!Util.hasToken(hdrs[idx].getValue(), tok))
                hdrs[idx] = new NVPair(hdr, hdrs[idx].getValue() + ", " + tok);
        }
    }
}
